/*
 * Miccsnookersim, a snooker simulation with AI
 * Copyright (C) 2009 Enno Ruijters, Frans van den Heuvel, Yannick Thimister,
 * Ali B. Ozmen, Esther Verhoef, Achim Leydecker
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Iterator;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.Color;

/**
 * Class representing a snooker table
 */

public class Table
{
	private GameLogic logic;
	private static final int TABLE_WIDTH = 360;
	private static final int TABLE_HEIGHT = TABLE_WIDTH / 2;
	private static final int CORNER_SIZE = TABLE_WIDTH / 40;
	private static final int EDGE_THICKNESS = TABLE_WIDTH / 80;
	private static final int HOLE_SIZE = TABLE_WIDTH / 40;
	private static final int TABLE_UPPER_BOUND = 2 * HOLE_SIZE;
	private static final int TABLE_LOWER_BOUND = TABLE_HEIGHT - 2*HOLE_SIZE;
	private static final int TABLE_LEFT_BOUND = 2 * HOLE_SIZE;
	private static final int TABLE_RIGHT_BOUND = TABLE_WIDTH - 2*HOLE_SIZE;
	private static final double EASY_DISTANCE = TABLE_WIDTH / 10;
	public static final double GAP_BETWEEN_BALLS = 0;
	public static final Vector TABLE_CENTER = new Vector(180, 90, 0);
	public static final Vector POSITION_YELLOW = new Vector(.75 * TABLE_WIDTH, TABLE_HEIGHT/3, 0);
	public static final Vector POSITION_GREEN = new Vector(.75 * TABLE_WIDTH, 2 * TABLE_HEIGHT/3, 0);
	public static final Vector POSITION_BROWN = new Vector(.75 * TABLE_WIDTH, TABLE_HEIGHT/2, 0);
	public static final Vector POSITION_BLUE = new Vector(TABLE_WIDTH/2, TABLE_HEIGHT/2, 0);
	public static final Vector POSITION_PINK = new Vector(TABLE_WIDTH/4, TABLE_HEIGHT/2, 0);
	public static final Vector POSITION_BLACK = new Vector(((TABLE_WIDTH/4) - (10 * Ball.RADIUS + 4))/2, (TABLE_HEIGHT/2), 0);
	
	private Ball[] balls;
	public ArrayList<Ball> stationaryBalls;
	public ArrayList<Ball> movingBalls;
	private ArrayList<TableListener> listeners;

	private double[][] frictionModifiers;

	private static final int[] xArrayLeft = {
		CORNER_SIZE+HOLE_SIZE/6,
		TABLE_WIDTH / 2 - EDGE_THICKNESS,
		TABLE_WIDTH / 2 - HOLE_SIZE,
		2*HOLE_SIZE
	};
	private static final int[] yArrayUp = {
		EDGE_THICKNESS,
		EDGE_THICKNESS,
		CORNER_SIZE,
		CORNER_SIZE
	};
	private static final int[] xArrayRight = {
		TABLE_WIDTH/2+EDGE_THICKNESS,
		TABLE_WIDTH-CORNER_SIZE-HOLE_SIZE/6,
		TABLE_WIDTH-2*HOLE_SIZE,
	       	TABLE_WIDTH/2+HOLE_SIZE
	};
	private static final int[] yArrayDown = {
		TABLE_HEIGHT-EDGE_THICKNESS,
		TABLE_HEIGHT-EDGE_THICKNESS,
		TABLE_HEIGHT-HOLE_SIZE,
		TABLE_HEIGHT-HOLE_SIZE
	};
	private static final int[] xArrayMidLeft = {
		EDGE_THICKNESS,
		HOLE_SIZE,
		HOLE_SIZE,
		EDGE_THICKNESS
	};
	private static final int[] yArrayMid = {
		CORNER_SIZE+HOLE_SIZE/6,
		2*CORNER_SIZE,
		TABLE_HEIGHT-(2*CORNER_SIZE),
		TABLE_HEIGHT-(CORNER_SIZE+HOLE_SIZE/6)
	};
	private static final int[] xArrayMidRight = {
		TABLE_WIDTH-(EDGE_THICKNESS),
		TABLE_WIDTH-(HOLE_SIZE),
		TABLE_WIDTH-(HOLE_SIZE),
		TABLE_WIDTH-(EDGE_THICKNESS)
	};

	public double getFrictionModifier(double x, double y)
	{
		if (x < 0)
			x = 0;
		if (((int) x) >= TABLE_WIDTH)
			x = TABLE_WIDTH - 1;
		if (y < 0)
			y = 0;
		if (((int) y) >= TABLE_HEIGHT)
			y = TABLE_HEIGHT - 1;
		return frictionModifiers[(int)x][(int)y];
	}

	private void initBalls()
	{
		this.balls = new Ball[22];
		movingBalls = new ArrayList<Ball>();
		
		// coloured balls
		balls[0] = new Ball(POSITION_YELLOW, Color.YELLOW, 2);
		balls[1] = new Ball(POSITION_GREEN, Color.GREEN, 3);
		balls[2] = new Ball(POSITION_BROWN, new Color(107, 66, 38), 4);
		balls[3] = new Ball(POSITION_BLUE, Color.BLUE, 5);
		balls[4] = new Ball(POSITION_PINK, Color.MAGENTA, 6);
		balls[5] = new Ball(POSITION_BLACK, Color.BLACK, 7);
		// red balls	
		balls[6] = new Ball(new Vector((TABLE_WIDTH/4) - (2 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), TABLE_HEIGHT/2, 0), Color.RED, 1);
		balls[7] = new Ball(new Vector((TABLE_WIDTH/4) - (4 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) - (Ball.RADIUS + GAP_BETWEEN_BALLS), 0), Color.RED, 1);
		balls[8] = new Ball(new Vector((TABLE_WIDTH/4) - (4 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) + (Ball.RADIUS + GAP_BETWEEN_BALLS), 0), Color.RED, 1);
		balls[9] = new Ball(new Vector((TABLE_WIDTH/4) - (6 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) - (2 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[10] = new Ball(new Vector((TABLE_WIDTH/4) - (6 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), TABLE_HEIGHT/2, 0), Color.RED, 1);
		balls[11] = new Ball(new Vector((TABLE_WIDTH/4) - (6 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) + (2 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[12] = new Ball(new Vector((TABLE_WIDTH/4) - (8 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) - (3 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[13] = new Ball(new Vector((TABLE_WIDTH/4) - (8 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) - (Ball.RADIUS + GAP_BETWEEN_BALLS), 0), Color.RED, 1);
		balls[14] = new Ball(new Vector((TABLE_WIDTH/4) - (8 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) + (Ball.RADIUS + GAP_BETWEEN_BALLS), 0), Color.RED, 1);
		balls[15] = new Ball(new Vector((TABLE_WIDTH/4) - (8 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) + (3 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[16] = new Ball(new Vector((TABLE_WIDTH/4) - (10 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) - (4 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[17] = new Ball(new Vector((TABLE_WIDTH/4) - (10 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) - (2 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[18] = new Ball(new Vector((TABLE_WIDTH/4) - (10 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2), 0), Color.RED, 1);
		balls[19] = new Ball(new Vector((TABLE_WIDTH/4) - (10 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) + (2 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);
		balls[20] = new Ball(new Vector((TABLE_WIDTH/4) - (10 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), (TABLE_HEIGHT/2) + (4 * (Ball.RADIUS + GAP_BETWEEN_BALLS)), 0), Color.RED, 1);

		stationaryBalls = new ArrayList<Ball>(22);
		for (int i = 0; i <= 20; i++)
			stationaryBalls.add(balls[i]);
		movingBalls = new ArrayList<Ball>();
	}

	private boolean isPathBlocked(Line2D bound1, Line2D bound2, Ball ballA,
	                              Ball ballB)
	{
		for (Ball b : balls) {
			if (b.getPosition().getElements()[0] != 1.0/0.0 &&
			    b != ballA && b != ballB) {
				Point2D position = b.getPosition().toPoint();
				double d1 = bound1.ptSegDist(position);
				double d2 = bound2.ptSegDist(position);
				if (d1 < Ball.RADIUS && d2 < Ball.RADIUS) {
					return true;
				}
			}
		}
		for (Polygon cushion : cushions) {
			int i;
			for (i = 1; i < cushion.npoints; i++) {
				if (bound1.intersectsLine(cushion.xpoints[i-1],
				                          cushion.ypoints[i-1],
				                          cushion.xpoints[i],
				                          cushion.ypoints[i]))
					return true;
				if (bound2.intersectsLine(cushion.xpoints[i-1],
				                          cushion.ypoints[i-1],
				                          cushion.xpoints[i],
				                          cushion.ypoints[i]))
					return true;
			}
			if (bound1.intersectsLine(cushion.xpoints[i-1],
			                          cushion.ypoints[i-1],
			                          cushion.xpoints[0],
			                          cushion.ypoints[0]))
				return true;
			if (bound2.intersectsLine(cushion.xpoints[i-1],
			                          cushion.ypoints[i-1],
			                          cushion.xpoints[0],
			                          cushion.ypoints[0]))
				return true;
		}
		return false;
	}

	public boolean canHit(Ball ballA, Ball ballB)
	{
		Vector positionA = ballA.getPosition();
		if (positionA.getElements()[0] == 1.0 / 0.0)
			return false;
		Vector positionB = ballB.getPosition();
		if (positionB.getElements()[0] == 1.0 / 0.0)
			return false;
		Vector centerLine = Vector.subtract(positionB, positionA);
		Vector delta = centerLine.rotate_(Math.PI);
		delta = delta.scaleTo_(ballA.getRadius());
		Vector tmpA, tmpB;
		tmpA = Vector.add(positionA, delta);
		tmpB = Vector.subtract(positionB, delta);
		Line2D.Double line = new Line2D.Double(tmpA.toPoint(),
		                                       tmpB.toPoint());
		tmpA = tmpA.sub_(delta);
		tmpA = tmpA.sub_(delta);
		tmpB = tmpB.sub_(delta);
		tmpB = tmpB.sub_(delta);
		Line2D.Double line2 = new Line2D.Double(tmpA.toPoint(),
		                                        tmpB.toPoint());
		if (!isPathBlocked(line, line2, ballA, ballB))
			return true;
		tmpA = Vector.subtract(positionA, delta);
		tmpB = Vector.add(positionB, delta);
		line = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		tmpA = tmpA.add_(delta);
		tmpA = tmpA.add_(delta);
		tmpB = tmpB.add_(delta);
		tmpB = tmpB.add_(delta);
		line2 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		if (!isPathBlocked(line, line2, ballA, ballB))
			return true;
		tmpA = Vector.add(positionA, delta);
		tmpB = Vector.add(positionB, delta);
		line = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		tmpA = tmpA.sub_(delta);
		tmpA = tmpA.sub_(delta);
		tmpB = tmpB.sub_(delta);
		tmpB = tmpB.sub_(delta);
		line2 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		return !isPathBlocked(line, line2, ballA, ballB);
	}

	public boolean canPotAnywhere(Ball b)
	{
		for (Ellipse2D pocket : pockets) {
			if (canPotInPocket(b, pocket))
				return true;
		}
		return false;
	}

	public boolean canPotInPocket(Ball b, Ellipse2D pot)
	{
		Vector pocketCenter = new Vector(pot.getCenterX(),
		                                 pot.getCenterY(),
		                                 0);
		Vector ballCenter = b.getPosition();
		double pocketRadius = pot.getHeight() / 2;
		Vector centerLine = Vector.subtract(pocketCenter, ballCenter);
		Vector pocketDelta = centerLine.rotate(Math.PI / 2);
		Vector ballDelta = pocketDelta.clone().scaleTo_(Ball.RADIUS);
		Line2D line1, line2;
		Vector tmpA, tmpB;
		pocketDelta = pocketDelta.scaleTo_(pocketRadius);

		/* Straight in */
		tmpA = Vector.add(ballCenter, ballDelta);
		tmpB = Vector.add(pocketCenter, ballDelta);
		line1 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		tmpA = Vector.subtract(ballCenter, ballDelta);
		tmpB = Vector.subtract(pocketCenter, ballDelta);
		line2 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		if (!isPathBlocked(line1, line2, b, null))
			return true;

		/* Edge */
		tmpA = Vector.add(ballCenter, ballDelta);
		tmpB = Vector.add(pocketCenter, pocketDelta);
		line1 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		tmpA = Vector.subtract(ballCenter, ballDelta);
		tmpB = Vector.add(pocketCenter, pocketDelta);
		tmpB = tmpB.sub_(ballDelta);
		tmpB = tmpB.sub_(ballDelta);
		line2 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		if (!isPathBlocked(line1, line2, b, null))
			return true;

		/* Other edge */
		tmpA = Vector.subtract(ballCenter, ballDelta);
		tmpB = Vector.add(pocketCenter, pocketDelta);
		line1 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		tmpA = Vector.subtract(ballCenter, ballDelta);
		tmpB = Vector.add(pocketCenter, pocketDelta);
		tmpB = tmpB.sub_(ballDelta);
		tmpB = tmpB.sub_(ballDelta);
		line2 = new Line2D.Double(tmpA.toPoint(), tmpB.toPoint());
		return  !isPathBlocked(line1, line2, b, null);
	}

	public boolean isEasilyPottable(Ball b)
	{
		for (Ellipse2D pocket : pockets) {
			Vector pocketCenter = new Vector(pocket.getCenterX(),
			                                 pocket.getCenterY(),
			                                 0);
			if (Vector.subtract(b.getPosition(), pocketCenter).magnitude() < EASY_DISTANCE && canPotInPocket(b, pocket))
				return true;
		}
		return false;
	}

	public int numEasyBalls()
	{
		int ret = 0;
		for (Ball b : balls) {
			if (b.getColor() != Color.WHITE)
				if (isEasilyPottable(b))
					ret += 1;
		}
		return ret;
	}

	public void shotBegin()
	{
		for (TableListener l : listeners)
			l.shotBegin();
	}

	public void shotEnd()
	{
		for (TableListener l : listeners)
			l.shotEnd();
	}

	private void initFrictionMap()
	{
		frictionModifiers = new double[TABLE_WIDTH][TABLE_HEIGHT];
		double noiseStdDev = Game.getSettings().getFrictionNoise();

		for (int i = 0; i < TABLE_WIDTH; i++) {
			for (int j = 0; j < TABLE_HEIGHT; j++) {
				frictionModifiers[i][j]
					= Util.getRandomBoundedNormal(0,
					                              noiseStdDev,
								      -100,
								      100);
			}
		}
	}

	/**
	 * prepare a table with all balls in the right position, where the cue
	 * ball is placed at the specified position.
	 */
	public Table(Vector cueBallPosition)
	{
		initBalls();
		initFrictionMap();
		listeners = new ArrayList<TableListener>(1);
		balls[21] = new Ball(cueBallPosition, Color.WHITE, 0);
		stationaryBalls.add(balls[21]);
		logic = new GameLogic(this);
		listeners.add(logic);
	}

	/* This method is thread-safe */
	public Table(Ball[] balls, GameLogic gl, boolean makeFrictionMap)
	{
		if (makeFrictionMap)
			initFrictionMap();
		logic = gl;
		listeners = new ArrayList<TableListener>(1);
		if (logic != null)
			listeners.add(logic);
		this.balls = new Ball[balls.length];
		movingBalls = new ArrayList<Ball>(balls.length);
		stationaryBalls = new ArrayList<Ball>(balls.length);
		for (int i = 0; i < balls.length; i++) {
			this.balls[i] = balls[i].clone();
			if (this.balls[i].getPosition().magnitude() < Double.POSITIVE_INFINITY)
				stationaryBalls.add(this.balls[i]);
		}
	}

	public void addListener(TableListener l)
	{
		listeners.add(l);
	}

	public void removeListener(TableListener l)
	{
		listeners.remove(l);
	}

	public void setGameLogic(GameLogic gl)
	{
		logic = gl;
	}

	public GameLogic getGameLogic()
	{
		return logic;
	}
	
	/**
		Simple method that creates a copy of the table
		@return a table object that is in the same state as the original
		Note: results are undefined if any of the balls are moving
		when this function is called.
		This method is thread-safe with regard to itself.
	*/
	public Table clone()
	{
		Table copy = new Table(balls, logic, false);
		copy.listeners = new ArrayList<TableListener>();
		if (logic != null) {
			copy.logic = logic.clone(copy);
			copy.listeners.add(copy.logic);
		}
		copy.frictionModifiers = this.frictionModifiers;
		return copy;
	}

	public void makeBallMoving(Ball b)
	{
		stationaryBalls.remove(b);
		movingBalls.add(b);
	}

	/**
	 * Returns an array of all the balls on the table
	 */
	public Ball[] getBalls(boolean includeOffTableBalls)
	{
		if (includeOffTableBalls) {
			return balls;
		} else {
			int numBalls = 0;
			for (Ball b : movingBalls)
				if (b.getPosition().magnitude() < 1.0/0.0)
					numBalls++;
			for (Ball b : stationaryBalls)
				if (b.getPosition().magnitude() < 1.0/0.0)
					numBalls++;
			Ball[] ret = new Ball[numBalls];
			for (Ball b : movingBalls)
				if (b.getPosition().magnitude() < 1.0/0.0)
					ret[--numBalls] = b;
			for (Ball b : stationaryBalls)
				if (b.getPosition().magnitude() < 1.0/0.0)
					ret[--numBalls] = b;
			return ret;
		}
	}

	public Ball[] getBalls()
	{
		return getBalls(true);
	}

	public void setBalls(Ball[] balls)
	{
		stationaryBalls = new ArrayList<Ball>(balls.length);
		for (int i = 0; i < balls.length; i++) {
			stationaryBalls.add(balls[i]);
		}
	}

	public void checkCollisions()
	{
		ListIterator<Ball> movingIter = movingBalls.listIterator();
		Line2D bounds[] = getBounds();
		Ellipse2D pockets[] = getPockets();
		while (movingIter.hasNext()) {
			Ball a = movingIter.next();
			Iterator<Ball> remaining = movingBalls.listIterator(movingIter.nextIndex());
			while (remaining.hasNext()) {
				Ball b = remaining.next();
				if (Physics.areColliding(a, b)) {
					for (TableListener l : listeners)
						l.ballCollision(a, b);
					Physics.ballCollision(a, b);
				}
			}
			remaining = stationaryBalls.iterator();
			while (remaining.hasNext()) {
				Ball b = remaining.next();
				if (Physics.areColliding(a, b)) {
					for (TableListener l : listeners)
						l.ballCollision(a, b);
					Physics.ballCollision(a, b);
					movingIter.add(b);
					remaining.remove();
				}
			}
			double[] position = a.getPosition().getElements();
			if (position[0] > TABLE_LEFT_BOUND
			    && position[0] < TABLE_RIGHT_BOUND
			    && position[1] > TABLE_UPPER_BOUND
			    && position[1] < TABLE_LOWER_BOUND)
				continue;
			boolean bounced = false;
			for (int i = 0; i < bounds.length && !bounced; i++) {
				Line2D bound = bounds[i];
				if (Physics.areColliding(a, bound)){
					for (TableListener l : listeners)
						l.ballCollision(a, bound);
					Physics.ballEdgeCollision(a, bound);
					bounced = true;
				}
			}
			for (Ellipse2D pocket : pockets) {
				if (Physics.isBallInPocket(a, pocket)) {
					for (TableListener l : listeners)
						l.ballPocketed(a);
					a.setPosition(new Vector(1.0/0.0, 0,0));
					/* I'm assuming infinity is
					 * outside the table */
					a.setVelocity(new Vector(0,0,0));
					try {
						movingIter.remove();
					} catch(IllegalStateException e) {
					}
					break;
				}
			}
			if (a.getPosition().magnitude() < 1.0/0.0
			    && (a.getPosition().getElements()[0] < 0
			        || a.getPosition().getElements()[0]
				                                   > TABLE_WIDTH
			        || a.getPosition().getElements()[1] < 0
			        || a.getPosition().getElements()[2]
			                                      > TABLE_HEIGHT)) {
				for (TableListener l : listeners)
					l.ballPocketed(a);
				a.setPosition(new Vector(1.0/0.0, 0,0));
				/* I'm assuming infinity is
				 * outside the table */
				a.setVelocity(new Vector(0,0,0));
				movingIter.remove();
			}
		}
	}

	public void replaceBall(Ball b)
	{
		if (b.getVelocity().magnitude() != 0)
			movingBalls.add(b);
		else
			stationaryBalls.add(b);
	}

	public void advanceTime(double time, boolean withNoise)
	{
		Iterator<Ball> movingBalls = this.movingBalls.iterator();
		while (movingBalls.hasNext()) {
			Ball b = movingBalls.next();
			b.advanceTime(time, this, withNoise);
			if (b.isStationary()) {
				movingBalls.remove();
				stationaryBalls.add(b);
			}
		}
		checkCollisions(); /* Should that be automatic here? */
	}

	public boolean areBallsMoving()
	{
		return movingBalls.size() != 0;
	}


	private static final Rectangle[] corners  = {
		new Rectangle(0, 0, CORNER_SIZE, CORNER_SIZE),
		new Rectangle(0, TABLE_HEIGHT - CORNER_SIZE, CORNER_SIZE, CORNER_SIZE),
		new Rectangle(TABLE_WIDTH - CORNER_SIZE, 0, CORNER_SIZE, CORNER_SIZE),
		new Rectangle(TABLE_WIDTH - CORNER_SIZE, TABLE_HEIGHT - CORNER_SIZE,
		              CORNER_SIZE, CORNER_SIZE)
	};

	public static Rectangle[] createCorner()
	{
		return corners;
	}

	private static final Rectangle[] edges = {
		new Rectangle(0, 0, TABLE_WIDTH, EDGE_THICKNESS),
		new Rectangle(0, 0, EDGE_THICKNESS, TABLE_HEIGHT),
		new Rectangle(0, TABLE_HEIGHT - EDGE_THICKNESS, TABLE_WIDTH,
		              EDGE_THICKNESS),
		new Rectangle(TABLE_WIDTH - EDGE_THICKNESS, 0, EDGE_THICKNESS,
		              TABLE_HEIGHT)
	};

	public static Rectangle[] createEdges()
	{
		return edges;
	}

	private static final Ellipse2D.Double[] pockets = {
		new Ellipse2D.Double(EDGE_THICKNESS / 3, EDGE_THICKNESS / 3,
		                     HOLE_SIZE, HOLE_SIZE),
		new Ellipse2D.Double(TABLE_WIDTH / 2 - 0.5 * HOLE_SIZE,
		                     EDGE_THICKNESS / 3,
							 HOLE_SIZE, HOLE_SIZE),
		new Ellipse2D.Double(TABLE_WIDTH - HOLE_SIZE - EDGE_THICKNESS / 3,
		                     EDGE_THICKNESS / 3,
		                     HOLE_SIZE, HOLE_SIZE),
		new Ellipse2D.Double(EDGE_THICKNESS / 3,
		                     TABLE_HEIGHT - HOLE_SIZE - EDGE_THICKNESS / 3,
		                     HOLE_SIZE, HOLE_SIZE),
		new Ellipse2D.Double(TABLE_WIDTH / 2 - 0.5 * HOLE_SIZE,
		                     TABLE_HEIGHT - HOLE_SIZE - EDGE_THICKNESS / 3,
		                     HOLE_SIZE, HOLE_SIZE),
		new Ellipse2D.Double(TABLE_WIDTH - HOLE_SIZE - EDGE_THICKNESS / 3,
		                     TABLE_HEIGHT - HOLE_SIZE - EDGE_THICKNESS / 3,
		                     HOLE_SIZE, HOLE_SIZE)
	};

	public static Ellipse2D.Double[] getPockets()
	{
		return pockets;
	}

	private static final Polygon[] cushions = {
		new Polygon(xArrayLeft, yArrayUp, 4),
		new Polygon(xArrayRight, yArrayUp, 4),
		new Polygon(xArrayLeft, yArrayDown, 4),
		new Polygon(xArrayRight, yArrayDown, 4),
		new Polygon(xArrayMidLeft, yArrayMid, 4),
		new Polygon(xArrayMidRight, yArrayMid, 4)
	};

	public static Polygon[] createCushion()
	{
		return cushions;
	}

	private static final Line2D.Double baulkLine =
			new Line2D.Double(TABLE_WIDTH*0.75, CORNER_SIZE,
			                  TABLE_WIDTH*0.75, TABLE_HEIGHT - CORNER_SIZE);

	public static Line2D.Double createBaulkLine()
	{
		return baulkLine;
	}

	public static int getTableWidth()
	{
		return TABLE_WIDTH;
	}

	public static int getTableHeight()
	{
		return TABLE_HEIGHT;
	}

	private static final Line2D.Double[] bounds = {
		new Line2D.Double(xArrayLeft[0], yArrayUp[0],
		                  xArrayLeft[3], yArrayUp[3]),
		new Line2D.Double(xArrayLeft[3], yArrayUp[3],
		                  xArrayLeft[2], yArrayUp[2]),
		new Line2D.Double(xArrayLeft[2], yArrayUp[2],
		                  xArrayLeft[1], yArrayUp[1]),

		new Line2D.Double(xArrayRight[0], yArrayUp[0],
		                  xArrayRight[3], yArrayUp[3]),
		new Line2D.Double(xArrayRight[3], yArrayUp[3],
		                  xArrayRight[2], yArrayUp[2]),
		new Line2D.Double(xArrayRight[2], yArrayUp[2],
		                  xArrayRight[1], yArrayUp[1]),

		new Line2D.Double(xArrayLeft[0], yArrayDown[0],
		                  xArrayLeft[3], yArrayDown[3]),
		new Line2D.Double(xArrayLeft[3], yArrayDown[3],
		                  xArrayLeft[2], yArrayDown[2]),
		new Line2D.Double(xArrayLeft[2], yArrayDown[2],
		                  xArrayLeft[1], yArrayDown[1]),

		new Line2D.Double(xArrayRight[0], yArrayDown[0],
		                  xArrayRight[3], yArrayDown[3]),
		new Line2D.Double(xArrayRight[3], yArrayDown[3],
		                  xArrayRight[2], yArrayDown[2]),
		new Line2D.Double(xArrayRight[2], yArrayDown[2],
		                  xArrayRight[1], yArrayDown[1]),

		new Line2D.Double(xArrayMidLeft[0], yArrayMid[0],
		                  xArrayMidLeft[1], yArrayMid[1]),
		new Line2D.Double(xArrayMidLeft[1], yArrayMid[1],
		                  xArrayMidLeft[2], yArrayMid[2]),
		new Line2D.Double(xArrayMidLeft[2], yArrayMid[2],
		                  xArrayMidLeft[3], yArrayMid[3]),

		new Line2D.Double(xArrayMidRight[0], yArrayMid[0],
		                  xArrayMidRight[1], yArrayMid[1]),
		new Line2D.Double(xArrayMidRight[1], yArrayMid[1],
		                  xArrayMidRight[2], yArrayMid[2]),
		new Line2D.Double(xArrayMidRight[2], yArrayMid[2],
		                  xArrayMidRight[3], yArrayMid[3])
	};


	public static Line2D.Double[] getBounds()
	{
		return bounds;
	}
}
